#!/usr/bin/env python
from evo.Combinator import Combinator
from io.GenOutput import spit_grammar_to_str
import random
import operator
import logging
import logging.handlers
import anydbm

class Pastor:
    
    LOG_FILENAME = 'pastor.log'
    # Set up a specific logger with our desired output level
    logger = logging.getLogger('pastor')
    logger.setLevel(logging.DEBUG)
    handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=200000, backupCount=5)
    logger.addHandler(handler)
        
    def __init__(self, mutator):
        self.initial_pop = []
        self.current_pop = []
        self.offspring = []
        self.combinator = Combinator()
        self.mutator = mutator
        
        self.pc_index = anydbm.open('pc_index', 'n')
        self.idcount = 0
        
        self.evals = []
        #self.fitness_list = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
        self.fitness_list = [ 0.0 for i in range(50)] # fit list fill of zeros
        
        self.pop_limit = 10 # magic number TODO: make it an external parameter
    
    def init(self):
        self.current_pop = self.initial_pop
    
    def add_eval(self, eval):
        self.evals.append(eval)
    
    def next_gen(self):
        self.current_pop = self.offspring
        self.offspring = []
        self.fitness_list = [ 0.0 for i in range(50)] # reset fitnesses
        self.evals = []

    def add_individual(self, graph):
        id = self.new_id()
        graph.set_name(id)
        self.initial_pop.append(graph)
        self.pc_index[id] = "ID00"       
    
    def eval_pop(self):
        """
        population evaluation process. asks user for fitness scores. builds cumulative probability list for
        roulette selection
        """
        self.logger.info("CURRENT_POP:\n" + print_pop(self.current_pop))
        self.logger.info(print_pop_ext(self.current_pop))
        
        #self.evals = input("Insert your evaluation pairs: (<individual#>,<score 0-9>) separated by commas, surrounded with [ ]:") # get user evaluation input. list of tuples e.g. [(2,3),(6,7),(1,9)]
        self.evals = sorted(self.evals, key=operator.itemgetter(1))        
        for eval_tuple in self.evals:   #for each evaluation input tuple
            self.fitness_list[eval_tuple[0]] = eval_tuple[1] # build fitness list
        
        self.offspring.append(self.current_pop[int(self.evals[0][0])]) # adding to next_pop. cast to int in order to use as list index 
        self.offspring.append(self.current_pop[int(self.evals[1][0])])
        
        fitness_sum = reduce(operator.add, self.fitness_list) # calculate fitness sum
        self.logger.debug("FITNESS SUM: " + str(fitness_sum))
        prob_list = map((lambda x: x / fitness_sum), self.fitness_list)
        self.logger.debug("PROB_LIST: " + str(prob_list))
        cum_value = 0
        self.cum_prob_list = []
        for prob in prob_list: # build cumulative probability list for roulette selection
            self.cum_prob_list.append(cum_value + prob)
            cum_value += prob
        
        self.cum_prob_list[-1] = 1.0 # compensate for round-off error

    def build_offspring(self):
        while(len(self.offspring) < self.pop_limit):
            
            self.logger.debug("BUILD OFFSPRING. LEN: " + str(len(self.offspring)) + " -------------------------------------------------")
            parents = self.select_parents()
            
            parent_a = self.current_pop[parents[0]]
            assert parent_a != None
            
            parent_b = self.current_pop[parents[1]]
            assert parent_b != None
            
            self.logger.debug("SELECTED: " + str(parents) + "\nA:\n")
            self.logger.debug(parent_a.print_graph())
            self.logger.debug("B:\n")
            self.logger.debug(parent_b.print_graph())
            
            self.logger.debug("CURRENT_POP:\n" + print_pop(self.current_pop))
            
            children = self.combinator.two_point_xover(parent_a, parent_b, 1, 3) # call two-point crossover on parents
            child_a = children[0] # extract both children from return tuple
            child_b = children[1]
            
            self.mutate(child_a) # mutate childs
            self.mutate(child_b)  
            
            #update parent-child index   
            id_a = self.new_id()
            child_a.set_name(id_a)
            id_b = self.new_id()
            child_b.set_name(id_b)         
            
            parents = parent_a.name() + parent_b.name()          
            self.pc_index[id_a] = parents
            self.pc_index[id_b] = parents
            
          
            self.offspring.extend([child_a, child_b]) # append both children to offspring    
    
    def select_parents(self):
        """
        roulette selection of two parents
        """
        selected = []       
        for i in xrange(2):
            rn = random.random()
            for j, cum_prob in enumerate(self.cum_prob_list):
                self.logger.debug("j: " + str(j) + ", cum_prob: " + str(cum_prob))
                if rn <= cum_prob:
                    selected.append(j)
                    break
        
        return (selected[0], selected[1]) # return tuple with parents

    def makelove(self):
        for pair in xselections(self.initial_pop, 2):
            self.combinator.two_point_xover(pair[0], pair[1], 1, 3)
    
    def new_id(self):
        return str('ID' + str(self.idcount) + str(1))
    
    def mutate(self, grapho):
        if roulette(20):
            self.mutator.mutate_duplicate_node(grapho)
        
        if roulette(20):
            self.mutator.mutate_remove_node(grapho)
        
        if roulette(5):
            self.mutator.mutate_startshape(grapho)
        
        for node in grapho.nodes_iter():
            if roulette(3):
                self.mutator.mutate_add_edge(grapho, node)
            
            if roulette(5):
                self.mutator.mutate_remove_edge(grapho, node)
            
            for instance in grapho[node].instance_list():
                for edge in grapho[node][instance]:
                    
                    if roulette(1):
                        self.mutator.mutate_change_param(edge)
                        
                    if roulette(1):
                        self.mutator.mutate_remove_param(edge)
                    
                    if roulette(1):
                        self.mutator.mutate_add_param(edge)

def roulette(odd):
    n = random.randint(1, 100)
    return n < odd

def print_pop(pop):
    ret = ""
    i = -1
    for graph in pop:
        i += 1
        ret += str(i) + ": " + graph.name() + "; "
    return ret

def print_pop_ext(pop):
    ret = ""
    i = -1
    for graph in pop:
        i += 1
        ret += str(i) + ": " + graph.print_graph() + "\n"
    return ret

def print_tuple_list(lst):
    ret = ""
    for tuple in lst:
        ret += str(tuple[0]) + " -> " + str(tuple[1]) + " : "
    return ret

def xselections(items, n):
    if n == 0: yield []
    else:
        for i in xrange(len(items)):
            for ss in xselections(items, n - 1):
                yield [items[i]] + ss   
